package io.kwdb.sdk;

import io.kwdb.sdk.internal.*;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * ILP (InfluxDB Line Protocol) client for KWDB. Sends data using HTTP POST to the /write endpoint.
 * M3: Supports batching, retry, metrics, and security features.
 */
public class IlpClient implements KwdbClient {

  private static final Logger logger = Logger.getLogger(IlpClient.class.getName());

  private final Config config;
  private final HttpClient httpClient;
  private final IlpEncoder encoder;
  private final String writeUrl;
  private final RetryPolicy retryPolicy;
  private final Metrics metrics;
  private final Batcher<Point> batcher;
  private volatile boolean closed = false;

  /**
   * Create ILP client with configuration.
   *
   * @param config The configuration
   */
  public IlpClient(Config config) {
    this.config = config;
    this.encoder = new IlpEncoder(config.isIlpIntegerWithSuffix());
    this.metrics = Metrics.getInstance();

    // M3: Build HTTP client with security configuration
    HttpClient.Builder clientBuilder =
        HttpClient.newBuilder().connectTimeout(Duration.ofMillis(config.getTimeoutMillis()));

    // Apply TLS configuration if needed
    SecurityUtil.configureHttpClient(clientBuilder, config);
    this.httpClient = clientBuilder.build();

    // M3: Build write endpoint URL with proper scheme (http/https)
    this.writeUrl = SecurityUtil.buildIlpUrl(config);

    // M3: Initialize retry policy
    this.retryPolicy =
        new RetryPolicy(config.isEnableRetry(), config.getMaxRetries(), config.getBaseBackoffMs());

    // M3: Initialize batcher if enabled
    if (config.getMaxBatchSize() > 0) {
      this.batcher =
          new Batcher<>(
              config.getMaxBatchSize(),
              config.getLingerMs(),
              config.getMaxQueueSize(),
              config.getBackpressure(),
              this::submitBatch);
      logger.info("ILP client initialized with batcher enabled");
    } else {
      this.batcher = null;
      logger.info("ILP client initialized with direct write mode");
    }
  }

  @Override
  public Status write(Point point) {
    if (closed) {
      return Status.failed("Client is closed");
    }

    if (point == null) {
      return Status.invalidArgument("Point cannot be null");
    }

    // M3: Use batcher if enabled
    if (batcher != null) {
      boolean accepted = batcher.offer(point, config.getTimeoutMillis());
      if (accepted) {
        return Status.ok();
      } else {
        metrics.incrementDropped(1);
        return Status.resourceExhausted("Failed to enqueue point");
      }
    }

    // Direct write with retry
    return writeDirectWithRetry(point);
  }

  /** Write a single point directly with retry logic. */
  private Status writeDirectWithRetry(Point point) {
    String ilpData = encoder.encode(point);
    return writeRawWithRetry(ilpData);
  }

  /** Write raw ILP data with retry logic. */
  private Status writeRawWithRetry(String ilpData) {
    long startTime = System.currentTimeMillis();

    Status status =
        retryPolicy.execute(
            () -> {
              try {
                return sendHttpRequest(ilpData);
              } catch (Exception e) {
                logger.log(Level.WARNING, "Error sending HTTP request", e);
                if (e instanceof java.net.http.HttpTimeoutException) {
                  return Status.deadlineExceeded("Request timeout: " + e.getMessage());
                } else if (e instanceof IOException) {
                  return Status.unavailable("Network error: " + e.getMessage());
                } else if (e instanceof InterruptedException) {
                  Thread.currentThread().interrupt();
                  return Status.cancelled("Request interrupted");
                } else {
                  return Status.internal("Unexpected error: " + e.getMessage());
                }
              }
            });

    long elapsed = System.currentTimeMillis() - startTime;
    metrics.observeWriteLatency(elapsed);

    if (status.isOk()) {
      metrics.incrementWritesOk(1);
    } else {
      metrics.incrementWritesFailed(1, status.getCode().getValue());
    }

    return status;
  }

  /** Send HTTP request with security headers. */
  private Status sendHttpRequest(String ilpData) throws Exception {
    byte[] body = ilpData.getBytes(StandardCharsets.UTF_8);

    // M3: Apply compression if enabled
    HttpRequest.Builder requestBuilder =
        HttpRequest.newBuilder()
            .uri(URI.create(writeUrl))
            .timeout(Duration.ofMillis(config.getTimeoutMillis()))
            .header("Content-Type", "text/plain; charset=utf-8");

    // M3: Add authentication headers
    SecurityUtil.configureHttpSecurity(requestBuilder, config, true);

    // M3: Handle gzip compression
    if (config.isGzip()) {
      body = SecurityUtil.gzip(body);
      requestBuilder.header("Content-Encoding", "gzip");
    }

    HttpRequest request = requestBuilder.POST(HttpRequest.BodyPublishers.ofByteArray(body)).build();

    HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

    if (response.statusCode() == 204 || response.statusCode() == 200) {
      return Status.ok();
    } else if (response.statusCode() >= 400 && response.statusCode() < 500) {
      // Client error - don't retry
      return Status.invalidArgument("HTTP " + response.statusCode() + ": " + response.body());
    } else {
      // Server error - retryable
      return Status.unavailable("HTTP " + response.statusCode() + ": " + response.body());
    }
  }

  @Override
  public Status batchWrite(List<Point> points) {
    if (closed) {
      return Status.failed("Client is closed");
    }

    if (points == null || points.isEmpty()) {
      return Status.invalidArgument("Points list cannot be null or empty");
    }

    // M3: Direct batch write bypasses batcher
    return submitBatchDirectly(points);
  }

  /** Submit a batch of points directly (used by batchWrite and batcher). */
  private Status submitBatchDirectly(List<Point> points) {
    long startTime = System.currentTimeMillis();

    // Encode all points to ILP format
    String ilpData = encoder.encode(points);

    Status status = writeRawWithRetry(ilpData);

    long elapsed = System.currentTimeMillis() - startTime;
    metrics.observeBatchLatency(elapsed);
    metrics.observeBatchSize(points.size());

    if (status.isOk()) {
      metrics.incrementWritesOk(points.size());
    } else {
      metrics.incrementWritesFailed(points.size(), status.getCode().getValue());
    }

    return status;
  }

  /** Callback for batcher to submit accumulated points. */
  private void submitBatch(List<Point> batch) {
    if (batch.isEmpty()) {
      return;
    }

    Status status = submitBatchDirectly(batch);
    if (!status.isOk()) {
      logger.log(Level.WARNING, "Batch submission failed: {0}", status.getMessage());
    }
  }

  @Override
  public CompletableFuture<Status> writeAsync(Point point) {
    if (closed) {
      return CompletableFuture.completedFuture(Status.failed("Client is closed"));
    }

    if (point == null) {
      return CompletableFuture.completedFuture(Status.invalidArgument("Point cannot be null"));
    }

    try {
      // Encode point to ILP format
      String ilpData = encoder.encode(point);

      // Create HTTP request
      HttpRequest request =
          HttpRequest.newBuilder()
              .uri(URI.create(writeUrl))
              .timeout(Duration.ofMillis(config.getTimeoutMillis()))
              .header("Content-Type", "text/plain; charset=utf-8")
              .POST(HttpRequest.BodyPublishers.ofString(ilpData, StandardCharsets.UTF_8))
              .build();

      // Send async request
      return httpClient
          .sendAsync(request, HttpResponse.BodyHandlers.ofString())
          .thenApply(
              response -> {
                if (response.statusCode() == 204 || response.statusCode() == 200) {
                  return Status.ok();
                } else {
                  return Status.failed("HTTP " + response.statusCode() + ": " + response.body());
                }
              })
          .exceptionally(
              ex -> {
                // Unwrap CompletionException
                Throwable cause = ex.getCause() != null ? ex.getCause() : ex;

                if (cause instanceof java.net.http.HttpTimeoutException) {
                  return Status.deadlineExceeded("Request timeout: " + cause.getMessage());
                } else if (cause instanceof IOException) {
                  return Status.unavailable("Network error: " + cause.getMessage());
                } else if (cause instanceof InterruptedException) {
                  Thread.currentThread().interrupt();
                  return Status.cancelled("Request interrupted");
                } else {
                  return Status.internal("Unexpected error: " + cause.getMessage());
                }
              });

    } catch (Exception e) {
      return CompletableFuture.completedFuture(
          Status.internal("Failed to create request: " + e.getMessage()));
    }
  }

  @Override
  public QueryResult query(String sql) {
    // ILP client doesn't support query - only write operations
    return QueryResult.error(
        Status.unimplemented(
            "ILP client does not support query operations. Use GrpcKwdbClient for queries."));
  }

  @Override
  public void flush() {
    if (batcher != null && !closed) {
      batcher.flush();
    }
  }

  @Override
  public void close() {
    if (closed) {
      return;
    }

    closed = true;
    logger.info("Closing ILP client...");

    // M3: Close batcher first (will flush pending items)
    if (batcher != null) {
      try {
        batcher.close();
      } catch (Exception e) {
        logger.log(Level.WARNING, "Error closing batcher", e);
      }
    }

    // Log final metrics
    Metrics.MetricsSnapshot snapshot = metrics.getSnapshot();
    logger.info("Final metrics: " + snapshot);
  }

  /**
   * Check if client is closed.
   *
   * @return true if closed
   */
  public boolean isClosed() {
    return closed;
  }

  /**
   * Get the encoder used by this client.
   *
   * @return The ILP encoder
   */
  public IlpEncoder getEncoder() {
    return encoder;
  }

  /**
   * Test ILP write with direct HTTP POST. This is a utility method for testing.
   *
   * @param ilpData The raw ILP data to send
   * @return The status of the operation
   */
  public Status writeRaw(String ilpData) {
    if (closed) {
      return Status.failed("Client is closed");
    }

    if (ilpData == null || ilpData.isEmpty()) {
      return Status.invalidArgument("ILP data cannot be null or empty");
    }

    try {
      HttpRequest request =
          HttpRequest.newBuilder()
              .uri(URI.create(writeUrl))
              .timeout(Duration.ofMillis(config.getTimeoutMillis()))
              .header("Content-Type", "text/plain; charset=utf-8")
              .POST(HttpRequest.BodyPublishers.ofString(ilpData, StandardCharsets.UTF_8))
              .build();

      HttpResponse<String> response =
          httpClient.send(request, HttpResponse.BodyHandlers.ofString());

      if (response.statusCode() == 204 || response.statusCode() == 200) {
        return Status.ok();
      } else {
        return Status.failed("HTTP " + response.statusCode() + ": " + response.body());
      }

    } catch (java.net.http.HttpTimeoutException e) {
      return Status.deadlineExceeded("Request timeout: " + e.getMessage());
    } catch (IOException e) {
      return Status.unavailable("Network error: " + e.getMessage());
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      return Status.cancelled("Request interrupted");
    } catch (Exception e) {
      return Status.internal("Unexpected error: " + e.getMessage());
    }
  }
}
